8.3 Kodierung und Skalierung#

ML-Algorithmen können nur Zahlen verarbeiten. In diesem Kapitel werden wir uns zunächst damit beschäftigen, wie auch kategoriale Daten wie beispielsweise die Farbe eines Autos verarbeitet werden können. Da viele ML-Modelle empfindlich darauf reagieren, wenn die numerischen Werte in sehr unterschiedlichen Größenordnungen liegen, beschäftigen wiruns auch mit der Sklaierung von numerischen Daten.

Lernziele#

Lernziele

  • Sie können geordnete kategoriale (= ordinale) Daten mit Hilfe eines Dictionaries und der replace()-Methode als Zahlen kodieren.

  • Sie können ungeordnete kategoriale (= nominame) Daten mit Hilfe der get_dummies()-Methode als Zahlen kodieren. Diese Methode nennt man One-Hot-Kodierung.

  • Sie können numerische Daten skalieren, indem Sie

    • mit dem MinMaxScaler die Daten normieren oder

    • mit dem StandardScaler die Daten standardisieren.

Kodierung von kategorialen Daten#

Bei den Beispielen zur linearen Regression haben wir zur Prognose des Verkaufspreises nur numerische Daten genutzt, wie beispielsweise den Kilometerstand. Es gibt jedoch weitere Merkmale, die die Kaufentscheidung beeinflussen, wie der Kraftstofftyp (Diesel oder Benzin) und die Marke des Autos. Diese würden wir ebenfalls gerne in die Prognose des Preises einfließen lassen. Dazu müssen die kategorialen Daten, die in der Regel durch den Datentyp String gekennzeichnet sind, vorab in Integer oder Floats umgewandelt werden. Je nachdem, ob die kategorialen Daten geordnet oder ungeordnet sind, gibt es verschiedene Vorgehensweisen, wie wir uns im Folgenden anhand eines Beispiels erarbeiten.

Wir laden einen Datensatz mit Verkaufsdaten der Plattform Autoscout24.de. Sie können die csv-Datei hier herunterladen Download autoscout24_kodierung.csv und in das Jupyter Notebook importieren. Alternativ können Sie die csv-Datei auch über die URL importieren, wie es in der folgenden Code-Zelle gemacht wird. Mit der Methode .info()lassen wir uns anzeigen, welchen Datentyp die Merkmale haben.

import pandas as pd 

url = 'https://gramschs.github.io/book_ml4ing/data/autoscout24_kodierung.csv'
daten = pd.read_csv(url)

daten.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 930 entries, 0 to 929
Data columns (total 14 columns):
 #   Column                Non-Null Count  Dtype  
---  ------                --------------  -----  
 0   Marke                 930 non-null    object 
 1   Modell                930 non-null    object 
 2   Farbe                 930 non-null    object 
 3   Erstzulassung         930 non-null    object 
 4   Jahr                  930 non-null    int64  
 5   Preis (Euro)          930 non-null    int64  
 6   Leistung (kW)         930 non-null    int64  
 7   Leistung (PS)         930 non-null    int64  
 8   Getriebe              930 non-null    object 
 9   Kraftstoff            930 non-null    object 
 10  Verbrauch (l/100 km)  930 non-null    float64
 11  Kilometerstand (km)   930 non-null    float64
 12  Bemerkungen           930 non-null    object 
 13  Zustand               930 non-null    object 
dtypes: float64(2), int64(4), object(8)
memory usage: 101.8+ KB

Wir sehen

  • 8 Merkmale mit Datentyp object: Marke, Modell, Farbe, Erstzulassung, Getriebe, Kraftstoff, Bemerkungen, Zustand,

  • 4 Merkmale mit Datentyp int64: Jahr, Preis (Euro), Leistung (PS), Leistung (kW)

  • 2 Merkmale mit Datentyp float64: Verbrauch (l/100 km) und Kilometerstand (km).

Als erstes betrachten wir geordnete Daten.

Geordnete kategoriale Daten mit zwei Kategorien (binär ordinale Daten)#

Als erstes betrachten wir das Merkmal »Getriebe«. Mit der Methode .unique() ermitteln wir, wie viele verschiedene Kategorien es für dieses Merkmal gibt.

daten['Getriebe'].unique()
array(['Automatik', 'Schaltgetriebe'], dtype=object)

Es gibt nur zwei Kategorien: Automatik und Schaltgetriebe. Diese beiden Werte wollen wir durch Integer ersetzen:

  • Automatik –> 0 und

  • Schaltgetriebe –> 1.

Pandas bietet dazu die Methode replace() an. Bei der Verwendung dieser Methode darf sich der Datentyp nicht ändern (in Pandas Version 2 noch erlaubt, ab Version 3 verboten). Daher kodieren wir zunächst die Strings 'Automatik' und 'Schaltgetriebe' als die Strings '0' und '1'mit Hilfe eines Dictionaries:

getriebe_kodierung = {
  'Automatik': '0',
  'Schaltgetriebe': '1',
}

Dann verwenden wir replace(), um die Ersetzung vorzunehmen. Zuletzt wandeln wir die Strings '0' und '1' noch mit der Methode astype() in Integer um:

daten['Getriebe'] = daten['Getriebe'].replace(getriebe_kodierung)
daten['Getriebe'] = daten['Getriebe'].astype('int')

# Kontrolle
daten['Getriebe'].unique()
array([0, 1])

Geordnete kategoriale Daten (ordinale Daten)#

Für das Merkmal »Zustand« gibt es vier Kategorien.

daten['Zustand'].unique()
array(['Gebrauchtwagen', 'junger Gebrauchtwagen', 'Neuwagen',
       'Jahreswagen'], dtype=object)

Die vier Zustände haben eine Ordnung, denn ein Neuwagen ist wertvoller als ein Jahreswagen. Der Jahreswagen wiederum ist im Allgmeinen wertvoller als der junge Gebrauchtwagen. Am wenigsten wertvoll ist der Gebrauchtwagen. Durch diese Ordnung ist es sinnvoll, beim Kodieren der Zustände durch Integer die Ordnung beizubehalten. Ob wir jetzt die 0 für den Neuwagen vergeben und die 3 für den Gebrauchtwagen oder umgekehrt, ist Geschmackssache.

zustand_kodierung = {
  'Gebrauchtwagen': '0',
  'junger Gebrauchtwagen': '1', 
  'Jahreswagen': '2',
  'Neuwagen': '3'
}

daten['Zustand'] = daten['Zustand'].replace(zustand_kodierung)
daten['Zustand'] = daten['Zustand'].astype('int')

# Kontrolle
daten['Zustand'].unique()
array([0, 1, 3, 2])

Ungeordnete kategoriale Daten (nominale Daten): One-Hot-Kodierung#

Anders verhät es sich bei den ungeordnetem kategorialen Daten wie beispielsweise den Farben der Autos.

daten['Farbe'].unique()
array(['grau', 'grün', 'schwarz', 'blau', 'weiß', 'silber', 'rot',
       'braun', 'orange', 'gelb', 'gold', 'beige', 'bronze', 'violett'],
      dtype=object)

14 verschiedene Farben haben die Autos in dem Datensatz. Es wäre jedoch falsch, nun Integer von 0 bis 13 zu vergeben, denn das würde eine Ordnung der Farben voraussetzen, die es nicht gibt. Wir verwenden daher das Verfahren der One-Hot-Kodierung. Anstatt einer Spalte mir den Farben führen wir 14 neue Spalten mit den Farben ‘grau’, ‘grün’, ‘schwarz’, ‘blau’, usw. ein. Wenn ein Auto die Farbe ‘grau’ hat, notieren wir in der Spalte ‘grau’ in dieser Zeile eine 1 und in den übrigen 13 Spalten mit den anderen Farben eine 0. So können wir die Farben numerisch kodieren, ohne eine Ordnung der Farben einzuführen, die es nicht gibt. Pandas bietet dafür die Methode get_dummies()an. Schauen wir uns zunächst an, was diese Methode bewirkt.

pd.get_dummies(daten['Farbe'])
beige blau braun bronze gelb gold grau grün orange rot schwarz silber violett weiß
0 False False False False False False True False False False False False False False
1 False False False False False False False True False False False False False False
2 False False False False False False False False False False True False False False
3 False False False False False False False False False False True False False False
4 False False False False False False False False False False True False False False
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
925 False False False False False False True False False False False False False False
926 False False False False False False True False False False False False False False
927 False False False False False False False False False False True False False False
928 False False False False False False True False False False False False False False
929 False True False False False False False False False False False False False False

930 rows × 14 columns

Damit haben wir die Spalte »Farbe« nun durch 14 Spalten kodiert. Wir könnten nun im ursprünglichen Datensatz die Spalte »Farbe« löschen und die neuen 14 Spalten hinzufügen. Tatsächlich erledigt das Pandas bereits für uns, wenn wir die Methode etwas modifiziert aufrufen. Mit dem Argument data= übergeben wir nun den kompletten Datensatz und mit dem Argument columns= spezifizieren wir die Liste der ungeordneten kategorialen Daten, die One-Hot-kodiert werden sollen.

daten = pd.get_dummies(data=daten, columns=['Farbe'])
daten.head()
Marke Modell Erstzulassung Jahr Preis (Euro) Leistung (kW) Leistung (PS) Getriebe Kraftstoff Verbrauch (l/100 km) ... Farbe_gelb Farbe_gold Farbe_grau Farbe_grün Farbe_orange Farbe_rot Farbe_schwarz Farbe_silber Farbe_violett Farbe_weiß
0 ford Ford S-Max 12/2018 2018 19350 140 190 0 Diesel 5.2 ... False False True False False False False False False False
1 mercedes-benz Mercedes-Benz G 500 10/2018 2018 119900 310 421 0 Benzin 11.5 ... False False False True False False False False False False
2 mercedes-benz Mercedes-Benz E 250 05/2012 2012 18997 150 204 1 Diesel 5.1 ... False False False False False False True False False False
3 bmw BMW 325 05/2003 2003 11000 141 192 1 Benzin 9.6 ... False False False False False False True False False False
4 ford Ford S-Max 04/2017 2017 21980 132 179 0 Diesel 5.0 ... False False False False False False True False False False

5 rows × 27 columns

Die neuen Spaltennamen sind eine Kombination aus dem alten Spaltennamen »Farbe« und den Kategorien.

Skalierung von numerischen Daten#

Nachdem wir uns intensiv mit den kategorialen Daten beschäftigt haben, betrachten wir nun die numerischen Daten. Wir laden den Original-Datensatz und entfernen die kategorialen Daten.

url = 'https://gramschs.github.io/book_ml4ing/data/autoscout24_kodierung.csv'
daten = pd.read_csv(url)

daten = daten.drop(columns=['Marke', 'Modell', 'Farbe', 'Erstzulassung', 
                            'Getriebe', 'Kraftstoff','Bemerkungen', 'Zustand'])
daten.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 930 entries, 0 to 929
Data columns (total 6 columns):
 #   Column                Non-Null Count  Dtype  
---  ------                --------------  -----  
 0   Jahr                  930 non-null    int64  
 1   Preis (Euro)          930 non-null    int64  
 2   Leistung (kW)         930 non-null    int64  
 3   Leistung (PS)         930 non-null    int64  
 4   Verbrauch (l/100 km)  930 non-null    float64
 5   Kilometerstand (km)   930 non-null    float64
dtypes: float64(2), int64(4)
memory usage: 43.7 KB

Ein erster Blick auf die Daten zeigt bereits, dass die Eigenschaftswerte in unterschiedlichen Bereichen liegen.

daten.head()
Jahr Preis (Euro) Leistung (kW) Leistung (PS) Verbrauch (l/100 km) Kilometerstand (km)
0 2018 19350 140 190 5.2 100000.0
1 2018 119900 310 421 11.5 92500.0
2 2012 18997 150 204 5.1 128586.0
3 2003 11000 141 192 9.6 190000.0
4 2017 21980 132 179 5.0 85349.0

Der Verbrauch gemessen in Litern pro 100 Kilometer liegt zwischen 5 und 10, wohingegen der Kilometerstand die 100000 km übersteigt.Das zeigt auch die Übersicht der statistischen Kennzahlen:

daten.describe()
Jahr Preis (Euro) Leistung (kW) Leistung (PS) Verbrauch (l/100 km) Kilometerstand (km)
count 930.000000 930.000000 930.000000 930.000000 930.000000 930.000000
mean 2016.289247 22958.117204 119.869892 163.067742 6.016667 85817.368817
std 5.325487 17389.729506 58.155680 79.036180 1.593688 74230.260767
min 2000.000000 150.000000 37.000000 50.000000 3.500000 1.000000
25% 2013.000000 11990.000000 81.000000 110.000000 4.900000 28277.250000
50% 2018.000000 18970.000000 110.000000 150.000000 5.700000 72055.000000
75% 2020.000000 28735.000000 140.000000 190.000000 6.700000 124839.750000
max 2023.000000 122980.000000 450.000000 612.000000 14.900000 400000.000000

Damit ist auch der Boxplot nur noch schwer lesbar:

import plotly.express as px 

fig = px.box(daten)
fig.show()

Das hat auch Auswirkungen auf das Training der ML-Modelle. Daher beschäftigen wir uns nun mit der Skalierung von Daten.

Sind die Bereich der Daten von ihren Zahlenwerten sehr verschieden, sollten alle numerischen Werte in dieselbe Größenordnung gebracht werden. Dieser Vorgang heißt Skalieren der Daten. Gebräulich sind dabei zwei verschiedene Methoden:

  • Normierung und

  • Standardisierung.

Normierung#

Bei der Normierung wird festgelegt, dass alle Zahlenwerte in einem festen Intervall liegen. Besonders häufig wird das Intervall \([0,1]\) genommen. Die Verbrauch (l/ 100 km), der zwischen 3.5 und 14.9 liegt, würde so transformiert werden, dass das Minimum 3.5 der 0 entspricht und das Maximum 14.9 der 1. Genauso würde mit den anderen Eigenschaften verfahren werden. Wir nutzen zur praktischen Umsetzung Scikit-Learn.

Damit keine Informationen über die Testdaten in das Training des ML-Modells sickern (Data Leakage), wird die Normierung an das Minimum und das Maximum der Trainingsdaten angepasst und ggf. für die Testdaten angewendet. Damit können einzelne Testdaten auch außerhalb des Intervalls \([0,1]\) liegen. Wir splitten daher zunächst unsere Daten in Trainings- und Testdaten.

from sklearn.model_selection import train_test_split

daten_train, daten_test = train_test_split(daten, random_state=0)

Dann importieren wir die Klasse MinMaxScaler aus dem Untermodul sklearn.preprocessing und erzeugen ein MinMaxScaler-Objekt:

from sklearn.preprocessing import MinMaxScaler

# Auswahl Skalierungsmethode: Normierung
normierung = MinMaxScaler()

Jetzt wird das Minimum/Maximum jeder Spalte bestimmt, also der MinMaxScaler an die Trainingsdaten angepasst. Daher ist es nicht verwunderlich, dass die Methode fit() genannt wurde. Dem MinMaxScaler werden also die Trainingsdaten übergeben:

normierung.fit(daten_train)
MinMaxScaler()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Zuletzt erfolgt die Transformation der Daten mit der transform()-Methode. Dazu werden einmal die Trainingsdaten und einmal die Testdaten dem angepassten MinMaxScaler übergeben und die transformierten Daten in neuen Variablen gespeichert.

# Transformation der Trainungs- und Testdaten
X_train_normiert = normierung.transform(daten_train)
X_test_normiert = normierung.transform(daten_test)

Wir schauen in ‘X_train_normiert’ hinein:

print(X_train_normiert)
[[0.73913043 0.19881137 0.24939467 0.24911032 0.10619469 0.2603238 ]
 [0.52173913 0.46282667 0.60048426 0.59964413 1.         0.43312217]
 [0.39130435 0.054954   0.19612591 0.19572954 0.26548673 0.54499772]
 ...
 [0.52173913 0.11642107 0.10411622 0.10498221 0.07079646 0.29836399]
 [0.52173913 0.06622975 0.20096852 0.20106762 0.15044248 0.48629743]
 [0.56521739 0.140438   0.29782082 0.29893238 0.20353982 0.36249681]]

Die Normierung der Daten scheint funktioniert zu haben. Alle Werte liegen zwischen 0 und 1. Gleichzeitig haben wir aber die Pandas-DataFrame-Datenstruktur verloren. Die Normierung ist nicht für uns Menschen gedacht, sondern für den ML-Algorithmus. Daher nutzt Scikit-Learn die Transformation der Daten gleichzeitig für die Umwandlung in das speichereffizientere NumPy-Array, das für den ML-Algorithmus gebraucht wird.

Standardisierung#

Oft sind Daten normalverteilt. Die Standardisierung berücksichtigt das und transformiert nicht auf ein festes Intervall, sondern verschiebt den Mittelwert auf 0 und die Varianz auf 1. Die normalverteilten Daten werden also standardnormalverteilt. Auch das lassen wir Scikit-Learn erledigen:

from sklearn.preprocessing import StandardScaler

# Auswahl Skalierungsmethode: Standardisierung
standardisierung = StandardScaler()

# Analyse: jede Spalte wird auf ihr Minimum und ihre Maximum hin untersucht
# es werden immer die Trainingsdaten verwendet
standardisierung.fit(daten_train)

# Transformation der Trainungs- und Testdaten
X_train_standardisiert = standardisierung.transform(daten_train)
X_test_standardisiert = standardisierung.transform(daten_test)

print(X_train_standardisiert)
[[ 1.05411060e-01  6.39004580e-02  3.46860470e-01  3.41219572e-01
  -7.59326677e-01  2.43172311e-01]
 [-8.53747017e-01  1.90756199e+00  2.88123391e+00  2.87447279e+00
   5.74521458e+00  1.17279526e+00]
 [-1.42924186e+00 -9.40678755e-01 -3.76651551e-02 -4.45550293e-02
   3.99898498e-01  1.77466489e+00]
 ...
 [-8.53747017e-01 -5.11444287e-01 -7.01845780e-01 -7.00371851e-01
  -1.01693227e+00  4.47821435e-01]
 [-8.53747017e-01 -8.61938393e-01 -2.70828013e-03 -5.97756914e-03
  -4.37319684e-01  1.45886827e+00]
 [-6.61915402e-01 -3.43730157e-01  6.96429220e-01  7.01275866e-01
  -5.09112922e-02  7.92844041e-01]]

Auch hier geht die Pandas-DataFrame-Struktur verloren.

Zusammenfassung und Ausblick#

Kategoriale Daten müssen kodiert werden, damit sind in einem ML-Algorithmus verarbeitet werden können. Geordnete kategoriale (ordinale) Daen können dabei über ein Dictionary und die replace()-Methode kodiert werden. Für ungeordnete kategoriale (nominale) Daten muss die One-Hot-Kodierung verwendet werden.

Auch numerische Daten müssen häufig für ML-Algorithmen aufbereitet werden, vor allem, wenn die Daten in sehr unterschiedlichen Zahlenbereichen liegen. Bei den bisher eingeführten ML-Modellen lineare Regression und Entscheidungsbäumen ist die Skalierung der numerischen Daten nicht notwendig. Erst die nachfolgenden ML-Modelle werden davon Gebrauch machen.